/*
   Copyright 2009 Bloo. All Rights Reserved.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
package org.youtensils.client.plugin.applet;

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.regex.Pattern;

import org.youtensils.client.plugin.audio.FlvAudioStream;

/**
 * A download worker class which downloads and encodes videos.
 * @author Bobby Soares
 */
public class DownloadWorker implements Runnable{
	private ArrayList listeners;
	private String videoId, format;
	private boolean active = true;
	/**
	 * A constructor specifying the download listener and download item details.
	 * @param listener the download listener
	 * @param videoId the id of the video to download
	 * @param format the desired download format
	 */
	public DownloadWorker(DownloadListener listener, String videoId, String format){
		this.listeners = new ArrayList();
		this.videoId = videoId;
		this.format = format;
		this.active = true;
		this.addListener(listener);
	}
	/**
	 * Retrieves the value of a parameter from a URL QueryString.
	 * @param QueryString the URL QueryString
	 * @param ParameterName the name of the parameter
	 * @return
	 */
	private String getQueryStringParameter(String QueryString, String ParameterName){
		try{
			String[] pars = QueryString.split("&");
			for(int i=0; i<pars.length; i++){
				String[] par = pars[i].split("=");
				if(URLDecoder.decode(par[0], "UTF-8").equalsIgnoreCase(ParameterName)){
					if(par.length > 1){
						return URLDecoder.decode(par[1], "UTF-8");
					}else break;
				}
			}
		}catch(UnsupportedEncodingException x){}
		return null;
	}
	/**
	 * Retrieves details for a given video.
	 * @param VideoId the video id
	 * @return the video details encoded in a QueryString
	 */
	private String getVideoDetails(String VideoId){
		String Url = "http://www.youtube.com/get_video_info?&video_id=" + VideoId + "&el=embedded&ps=default&eurl=http%3A%2F%2Fwww%2Eyoutensils%2Eorg%2F";
		String Details = "";
		HttpURLConnection conn = null;
		BufferedReader rd = null;
		try {
			conn = getResourceConnection(Url);
			conn.connect();
	        rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
	        String buffer = "";
	        while ((buffer = rd.readLine()) != null) {
	            Details += buffer;
	        }
	    } catch (IOException e) {
	    	e.printStackTrace();
	    } finally {
	    	try{
		        if(rd != null) rd.close();
	    	}catch(IOException x){}
	    }
	    System.out.println("Got video details: " + Details);
	    return Details;
	}
	/**
	 * Constructs and initiates a web connection.
	 * @param Url the URL which should be requested
	 * @return an HttpURLConnection object
	 * @throws MalformedURLException if the supplied URL is invalid
	 * @throws IOException if an IO error is thrown when setting up the connection
	 */
	private HttpURLConnection getResourceConnection(String Url) throws MalformedURLException, IOException{
        HttpURLConnection.setFollowRedirects(true);
		URL url = new URL(Url);
		HttpURLConnection webConnection = (HttpURLConnection)url.openConnection();
        webConnection.setRequestProperty("Referer", "http://s.ytimg.com/yt/swf/cps-vfl98307.swf");
        webConnection.setRequestProperty("x-flash-version", "10,0,12,36");
        webConnection.setRequestProperty("User-Agent", "Mozilla/5.001 (windows; U; NT4.0; en-US; rv:1.0) Gecko/25250101");
        webConnection.setUseCaches(false);
        webConnection.setDoOutput(true);
        return webConnection;
	}
	/**
	 * Checks if this worker is active.
	 * @return a boolean value indicating if an item is currently downloading
	 * @throws DownloadCancelationException
	 */
	private boolean checkIsActive() throws DownloadCancelationException {
		if(!this.active) throw new DownloadCancelationException();
		return true;
	}
	/** Downloads a video in the specified format.
	 * @param videoId the id of the video to be downloaded
	 * @param format the desired download format
	 */
	private void downloadVideo(String videoId, String format){
		openItem();
		/* Define Variables */
		HttpURLConnection webConnection = null;
		FlvAudioStream audioStreamReader = null;
		InputStream streamReader = null;
		FileOutputStream streamWriter = null;
		String outputPath = null,
			resourceUrl = null,
			videoDetails = null,
			videoTitle = null,
			videoFormatUrlMap = null;
        Hashtable formatVideoUrls = new Hashtable();
		int read = 0, totalRead = 0, resourceSize = 0;
		byte[] buffer = new byte[8192];
		try{
				this.checkIsActive();
			/* Validate Format */
				if(!format.equalsIgnoreCase("FLV") &&
					!format.equalsIgnoreCase("MP4") &&
					!format.equalsIgnoreCase("MP3")){
					throw new DownloadErrorException("Invalid Video Format");
				}
				this.checkIsActive();
			/* Retrieve & Validate Video Info */
			setItemProgress(5, 1, 0, 0, "Validating Video Id.");
				if(!isValidVideoId(videoId)){
					throw new DownloadErrorException("Invalid Video Id");
				}
				this.checkIsActive();
			setItemProgress(5, 2, 0, 0, "Retrieving Video Information.");
				videoDetails = getVideoDetails(videoId);
				videoTitle = getQueryStringParameter(videoDetails, "title");
				videoFormatUrlMap = getQueryStringParameter(videoDetails, "fmt_url_map");
				System.out.println("Url Map: " + videoFormatUrlMap);
				if(videoFormatUrlMap == null || videoFormatUrlMap.equals("")){
					throw new DownloadErrorException("Could not retrieve video format map.");
				} else {
					String[] formatMaps = videoFormatUrlMap.split(",");
					for (int i=0; i<formatMaps.length; i++) {
						String[] formatMap = formatMaps[i].split("\\|");
						if (formatMap.length == 2) {
							formatVideoUrls.put(formatMap[0], formatMap[1]);
						}
					}
				}
				this.checkIsActive();
			/* Retrieve & Validate Output Location */
			setItemProgress(5, 3, 0, 0, "Requesting output file location.");
				//outputPath = getFilePath();
				outputPath = DownloadEngine.getUniqueValidHomeFilePath(videoTitle, format.toLowerCase());
				System.out.println("Path: " + outputPath);
				if(outputPath == null || outputPath.equals("")){
					throw new DownloadErrorException("Invalid output location specified.");
				}
				this.checkIsActive();
			/* Generate & Validate Download URL */
				if(format.equalsIgnoreCase("FLV")) {
					if (formatVideoUrls.containsKey("34")) {
						resourceUrl = (String) formatVideoUrls.get("34");
					} else {
						throw new DownloadErrorException("The target video is not available in " + format + " compatible format.");
					}
				} else if(format.equalsIgnoreCase("MP4")) {
					if (formatVideoUrls.containsKey("18")) {
						resourceUrl = (String) formatVideoUrls.get("18");
					} else {
						throw new DownloadErrorException("The target video is not available in " + format + " compatible format.");
					}
				} else if(format.equalsIgnoreCase("MP3")) {
					if (formatVideoUrls.containsKey("5")) {
						resourceUrl = (String) formatVideoUrls.get("5");
					} else {
						throw new DownloadErrorException("The target video is not available in " + format + " compatible format.");
					}
				}
				else throw new DownloadErrorException("Invalid format specified.");
				this.checkIsActive();
			/* Download Video */
				streamWriter = new FileOutputStream(outputPath);
				webConnection = getResourceConnection(resourceUrl);
				webConnection.connect();
				resourceSize = webConnection.getContentLength();
		        if(format.equalsIgnoreCase("MP3")){
        	setItemProgress(5, 4, resourceSize, 0, "Downloading and stripping audio.");
		        	audioStreamReader = new FlvAudioStream(webConnection.getInputStream());
		        	while(this.checkIsActive() && (read = audioStreamReader.read(buffer, 0, 8192)) > 0){
			        	streamWriter.write(buffer, 0, read);
			        	totalRead = (int)audioStreamReader.getInputOffset();
        	setItemProgress(5, 4, resourceSize, totalRead, "Downloading and stripping audio.");
			        }
		        }else{
        	setItemProgress(5, 4, resourceSize, 0, "Downloading");
		        	streamReader = webConnection.getInputStream();
		        	while(this.checkIsActive() && (read = streamReader.read(buffer, 0, 8192)) > 0){
			        	streamWriter.write(buffer, 0, read);
			        	totalRead += read;
        	setItemProgress(5, 4, resourceSize, totalRead, "Downloading.");
			        }
		        }
		        this.checkIsActive();
	        setItemProgress(5, 5, resourceSize, resourceSize, "Downloaded Video as " + format + " to " + outputPath + ".");
		}catch(DownloadCancelationException dcx){
			cancelItem();
			return;//don't close
		}catch(DownloadErrorException dex){
			errorItem(dex.getMessage());
		}catch(Exception x){
			x.printStackTrace();
			errorItem(x.getMessage());
		}finally{
	    	try{
		        if(streamReader != null) streamReader.close();
		        if(audioStreamReader != null) audioStreamReader.close();
		        if(streamWriter != null) streamWriter.close();
	    	}catch(IOException x){}
	    }
		closeItem();
	}
	/**
	 * Validates a video id.
	 * @param VideoId the video id to be downloaded
	 * @return a boolean value indicating if the video id is valid
	 */
	private boolean isValidVideoId(String VideoId){
		if(VideoId == null) return false;
		if(VideoId.length() < 10 || VideoId.length() > 20) return false;
		return Pattern.matches("[a-zA-Z0-9_\\-]+", VideoId);
	}
	/**
	 * Adds an event listener.
	 * @param listener the listener to register
	 */
	public void addListener(DownloadListener listener){
		this.listeners.add(listener);
	}
	/**
	 * Cancels the current download.
	 */
	public void cancel(){
		this.active = false;
	}
	/**
	 * Stops the current worker thread.
	 */
	public void stop(){
		this.cancel();
	}
	public void run() {
		try{
			this.downloadVideo(this.videoId, this.format);
		}catch(AccessControlException x){
			//running inside sandbox, abort
			errorItem(x.getMessage());
		}
	}
	/**
	 * Signals to all registered event listeners that an item has been opened.
	 */
	private void openItem(){
		for(int i=0; i<this.listeners.size(); i++){
			((DownloadListener)this.listeners.get(i)).onDownloadStart(this.videoId);
		}
	}
	/**
	 * Signals to all registered event listeners that an item has been closed.
	 */
	private void closeItem(){
		for(int i=0; i<this.listeners.size(); i++){
			((DownloadListener)this.listeners.get(i)).onDownloadComplete(this.videoId);
		}
	}
	/**
	 * Signals to all registered event listeners that an item has been canceled.
	 */
	private void cancelItem(){
		for(int i=0; i<this.listeners.size(); i++){
			((DownloadListener)this.listeners.get(i)).onDownloadCancel(this.videoId);
		}
	}
	/**
	 * Signals to all registered event listeners that an error has occurred.
	 * @param error the error description
	 */
	private void errorItem(String error){
		for(int i=0; i<this.listeners.size(); i++){
			((DownloadListener)this.listeners.get(i)).onDownloadError(this.videoId, error);
		}
	}
	/**
	 * Signals to all registered event listeners that an item's download progress has changed.
	 * @param totalSteps the total number of steps required to successful download completion
	 * @param currentStep the current step
	 * @param totalBytes the total byte size of the item being downloaded
	 * @param loadedBytes the amount of bytes that have loaded for the item being downloaded
	 * @param label a label describing the current step
	 */
	private void setItemProgress(int totalSteps, int currentStep, int totalBytes, int loadedBytes, String label){
		for(int i=0; i<this.listeners.size(); i++){
			((DownloadListener)this.listeners.get(i)).onDownloadProgress(this.videoId, totalSteps, currentStep, totalBytes, loadedBytes, label);
		}
	}
}